/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 *    ClassAssigner.java
 *    Copyright (C) 2002 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.gui.beans;

import weka.core.Instances;

import java.awt.BorderLayout;
import java.beans.EventSetDescriptor;
import java.io.Serializable;
import java.util.Vector;

import javax.swing.JPanel;

/**
 * Bean that assigns a class attribute to a data set.
 * 
 * @author Mark Hall
 * @version $Revision: 7439 $
 */
public class ClassAssigner extends JPanel implements Visible,
		DataSourceListener, TrainingSetListener, TestSetListener, DataSource,
		TrainingSetProducer, TestSetProducer, BeanCommon, EventConstraints,
		Serializable, InstanceListener, StructureProducer {

	/** for serialization */
	private static final long serialVersionUID = 4011131665025817924L;

	private String m_classColumn = "last";

	/** format of instances for current incoming connection (if any) */
	private Instances m_connectedFormat;

	private Object m_trainingProvider;
	private Object m_testProvider;
	private Object m_dataProvider;
	private Object m_instanceProvider;

	private Vector m_trainingListeners = new Vector();
	private Vector m_testListeners = new Vector();
	private Vector m_dataListeners = new Vector();
	private Vector m_instanceListeners = new Vector();

	private Vector m_dataFormatListeners = new Vector();

	protected transient weka.gui.Logger m_logger = null;

	protected BeanVisual m_visual = new BeanVisual("ClassAssigner",
			BeanVisual.ICON_PATH + "ClassAssigner.gif", BeanVisual.ICON_PATH
					+ "ClassAssigner_animated.gif");

	/**
	 * Global info for this bean
	 * 
	 * @return a <code>String</code> value
	 */
	public String globalInfo() {
		return Messages.getInstance()
				.getString("ClassAssigner_GlobalInfo_Text");
	}

	public ClassAssigner() {
		setLayout(new BorderLayout());
		add(m_visual, BorderLayout.CENTER);
	}

	/**
	 * Set a custom (descriptive) name for this bean
	 * 
	 * @param name
	 *            the name to use
	 */
	public void setCustomName(String name) {
		m_visual.setText(name);
	}

	/**
	 * Get the custom (descriptive) name for this bean (if one has been set)
	 * 
	 * @return the custom name (or the default name)
	 */
	public String getCustomName() {
		return m_visual.getText();
	}

	/**
	 * Tool tip text for this property
	 * 
	 * @return a <code>String</code> value
	 */
	public String classColumnTipText() {
		return Messages.getInstance().getString(
				"ClassAssigner_ClassColumnTipText_Text");
	}

	private Instances getUpstreamStructure() {
		if (m_dataProvider != null
				&& m_dataProvider instanceof StructureProducer) {
			return ((StructureProducer) m_dataProvider).getStructure("dataSet");
		}
		if (m_trainingProvider != null
				&& m_trainingProvider instanceof StructureProducer) {
			return ((StructureProducer) m_trainingProvider)
					.getStructure("trainingSet");
		}
		if (m_testProvider != null
				&& m_testProvider instanceof StructureProducer) {
			return ((StructureProducer) m_testProvider).getStructure("testSet");
		}
		if (m_instanceProvider != null
				&& m_instanceProvider instanceof StructureProducer) {
			return ((StructureProducer) m_instanceProvider)
					.getStructure("instance");
		}
		return null;
	}

	/**
	 * Get the structure of the output encapsulated in the named event. If the
	 * structure can't be determined in advance of seeing input, or this
	 * StructureProducer does not generate the named event, null should be
	 * returned.
	 * 
	 * @param eventName
	 *            the name of the output event that encapsulates the requested
	 *            output.
	 * 
	 * @return the structure of the output encapsulated in the named event or
	 *         null if it can't be determined in advance of seeing input or the
	 *         named event is not generated by this StructureProduce.
	 */
	public Instances getStructure(String eventName) {
		if (!eventName.equals("trainingSet") && !eventName.equals("testSet")
				&& !eventName.equals("dataSet")
				&& !eventName.equals("instance")) {
			return null;
		}
		if (m_trainingProvider == null && m_testProvider == null
				&& m_dataProvider == null && m_instanceProvider == null) {
			return null;
		}

		if (eventName.equals("dataSet") && m_dataListeners.size() == 0) {
			// downstream has asked for the structure of something that we
			// are not producing at the moment
			return null;
		}

		if (eventName.equals("trainingSet") && m_trainingListeners.size() == 0) {
			// downstream has asked for the structure of something that we
			// are not producing at the moment
			return null;
		}

		if (eventName.equals("testSet") && m_testListeners.size() == 0) {
			// downstream has asked for the structure of something that we
			// are not producing at the moment
			return null;
		}

		if (eventName.equals("instance") && m_instanceListeners.size() == 0) {
			// downstream has asked for the structure of something that we
			// are not producing at the moment
			return null;
		}

		if (m_connectedFormat == null) {
			m_connectedFormat = getUpstreamStructure();
		}

		assignClass(m_connectedFormat);
		return m_connectedFormat;
	}

	/**
	 * Returns the structure of the incoming instances (if any)
	 * 
	 * @return an <code>Instances</code> value
	 */
	public Instances getConnectedFormat() {
		// loaders will push instances format to us
		// when the user makes configuration changes
		// to the loader in the gui. However, if a fully
		// configured flow is loaded then we won't get
		// this information pushed to us until the
		// flow is run. In this case we want to pull
		// it (if possible) from upstream steps so
		// that our customizer can provide the nice
		// UI with the drop down box of class names.
		if (m_connectedFormat == null) {
			// try and pull the incoming structure
			// from the upstream step (if possible)
			m_connectedFormat = getUpstreamStructure();
		}

		return m_connectedFormat;
	}

	public void setClassColumn(String col) {
		m_classColumn = col;
		if (m_connectedFormat != null) {
			assignClass(m_connectedFormat);
		}
	}

	public String getClassColumn() {
		return m_classColumn;
	}

	public void acceptDataSet(DataSetEvent e) {
		Instances dataSet = e.getDataSet();
		assignClass(dataSet);
		notifyDataListeners(e);
		if (e.isStructureOnly()) {
			m_connectedFormat = e.getDataSet();
			// tell any listening customizers (or other
			notifyDataFormatListeners();
		}
	}

	public void acceptTrainingSet(TrainingSetEvent e) {
		Instances trainingSet = e.getTrainingSet();
		assignClass(trainingSet);
		notifyTrainingListeners(e);

		if (e.isStructureOnly()) {
			m_connectedFormat = e.getTrainingSet();
			// tell any listening customizers (or other
			notifyDataFormatListeners();
		}
	}

	public void acceptTestSet(TestSetEvent e) {
		Instances testSet = e.getTestSet();
		assignClass(testSet);
		notifyTestListeners(e);

		if (e.isStructureOnly()) {
			m_connectedFormat = e.getTestSet();
			// tell any listening customizers (or other
			notifyDataFormatListeners();
		}
	}

	public void acceptInstance(InstanceEvent e) {
		if (e.getStatus() == InstanceEvent.FORMAT_AVAILABLE) {
			// Instances dataSet = e.getInstance().dataset();
			m_connectedFormat = e.getStructure();

			// System.err.println("Assigning class column...");
			assignClass(m_connectedFormat);
			notifyInstanceListeners(e);

			// tell any listening customizers (or other interested parties)
			System.err.println(Messages.getInstance().getString(
					"ClassAssigner_AcceptInstance_Error_Text"));
			notifyDataFormatListeners();
		} else {
			// Instances dataSet = e.getInstance().dataset();
			// assignClass(dataSet);
			notifyInstanceListeners(e);
		}
	}

	private void assignClass(Instances dataSet) {
		int classCol = -1;
		if (m_classColumn.toLowerCase().compareTo("last") == 0) {
			dataSet.setClassIndex(dataSet.numAttributes() - 1);
		} else if (m_classColumn.toLowerCase().compareTo("first") == 0) {
			dataSet.setClassIndex(0);
		} else {
			classCol = Integer.parseInt(m_classColumn) - 1;
			if (/* classCol < 0 || */classCol > dataSet.numAttributes() - 1) {
				if (m_logger != null) {
					m_logger.logMessage(Messages.getInstance().getString(
							"ClassAssigner_AssignClass_LogMessage_Text"));
				}
			} else {
				dataSet.setClassIndex(classCol);
			}
		}
	}

	protected void notifyTestListeners(TestSetEvent tse) {
		Vector l;
		synchronized (this) {
			l = (Vector) m_testListeners.clone();
		}
		if (l.size() > 0) {
			for (int i = 0; i < l.size(); i++) {
				System.err.println(Messages.getInstance().getString(
						"ClassAssigner_NotifyTestListeners_Error_Text"));
				((TestSetListener) l.elementAt(i)).acceptTestSet(tse);
			}
		}
	}

	protected void notifyTrainingListeners(TrainingSetEvent tse) {
		Vector l;
		synchronized (this) {
			l = (Vector) m_trainingListeners.clone();
		}
		if (l.size() > 0) {
			for (int i = 0; i < l.size(); i++) {
				System.err.println(Messages.getInstance().getString(
						"ClassAssigner_NotifyTrainingListeners_Error_Text"));
				((TrainingSetListener) l.elementAt(i)).acceptTrainingSet(tse);
			}
		}
	}

	protected void notifyDataListeners(DataSetEvent tse) {
		Vector l;
		synchronized (this) {
			l = (Vector) m_dataListeners.clone();
		}
		if (l.size() > 0) {
			for (int i = 0; i < l.size(); i++) {
				System.err.println(Messages.getInstance().getString(
						"ClassAssigner_NotifyDataListeners_Error_Text"));
				((DataSourceListener) l.elementAt(i)).acceptDataSet(tse);
			}
		}
	}

	protected void notifyInstanceListeners(InstanceEvent tse) {
		Vector l;
		synchronized (this) {
			l = (Vector) m_instanceListeners.clone();
		}
		if (l.size() > 0) {
			for (int i = 0; i < l.size(); i++) {
				// System.err.println("Notifying instance listeners "
				// +"(ClassAssigner)");

				((InstanceListener) l.elementAt(i)).acceptInstance(tse);
			}
		}
	}

	protected void notifyDataFormatListeners() {
		Vector l;
		synchronized (this) {
			l = (Vector) m_dataFormatListeners.clone();
		}
		if (l.size() > 0) {
			DataSetEvent dse = new DataSetEvent(this, m_connectedFormat);
			for (int i = 0; i < l.size(); i++) {
				// System.err.println("Notifying instance listeners "
				// +"(ClassAssigner)");
				((DataFormatListener) l.elementAt(i)).newDataFormat(dse);
			}
		}
	}

	public synchronized void addInstanceListener(InstanceListener tsl) {
		m_instanceListeners.addElement(tsl);
		if (m_connectedFormat != null) {
			InstanceEvent e = new InstanceEvent(this, m_connectedFormat);
			tsl.acceptInstance(e);
		}
	}

	public synchronized void removeInstanceListener(InstanceListener tsl) {
		m_instanceListeners.removeElement(tsl);
	}

	public synchronized void addDataSourceListener(DataSourceListener tsl) {
		m_dataListeners.addElement(tsl);
		// pass on any format that we might know about
		if (m_connectedFormat != null) {
			DataSetEvent e = new DataSetEvent(this, m_connectedFormat);
			tsl.acceptDataSet(e);
		}
	}

	public synchronized void removeDataSourceListener(DataSourceListener tsl) {
		m_dataListeners.removeElement(tsl);
	}

	public synchronized void addTrainingSetListener(TrainingSetListener tsl) {
		m_trainingListeners.addElement(tsl);
		// pass on any format that we might know about
		if (m_connectedFormat != null) {
			TrainingSetEvent e = new TrainingSetEvent(this, m_connectedFormat);
			tsl.acceptTrainingSet(e);
		}
	}

	public synchronized void removeTrainingSetListener(TrainingSetListener tsl) {
		m_trainingListeners.removeElement(tsl);
	}

	public synchronized void addTestSetListener(TestSetListener tsl) {
		m_testListeners.addElement(tsl);
		// pass on any format that we might know about
		if (m_connectedFormat != null) {
			TestSetEvent e = new TestSetEvent(this, m_connectedFormat);
			tsl.acceptTestSet(e);
		}
	}

	public synchronized void removeTestSetListener(TestSetListener tsl) {
		m_testListeners.removeElement(tsl);
	}

	public synchronized void addDataFormatListener(DataFormatListener dfl) {
		m_dataFormatListeners.addElement(dfl);
	}

	public synchronized void removeDataFormatListener(DataFormatListener dfl) {
		m_dataFormatListeners.removeElement(dfl);
	}

	public void setVisual(BeanVisual newVisual) {
		m_visual = newVisual;
	}

	public BeanVisual getVisual() {
		return m_visual;
	}

	public void useDefaultVisual() {
		m_visual.loadIcons(BeanVisual.ICON_PATH + "ClassAssigner.gif",
				BeanVisual.ICON_PATH + "ClassAssigner_animated.gif");
	}

	/**
	 * Returns true if, at this time, the object will accept a connection
	 * according to the supplied event name
	 * 
	 * @param eventName
	 *            the event
	 * @return true if the object will accept a connection
	 */
	public boolean connectionAllowed(String eventName) {
		if (eventName.compareTo("trainingSet") == 0
				&& (m_trainingProvider != null || m_dataProvider != null || m_instanceProvider != null)) {
			return false;
		}

		if (eventName.compareTo("testSet") == 0 && m_testProvider != null) {
			return false;
		}

		if (eventName.compareTo("instance") == 0 && m_instanceProvider != null
				|| m_trainingProvider != null || m_dataProvider != null) {
			return false;
		}
		return true;
	}

	/**
	 * Returns true if, at this time, the object will accept a connection
	 * according to the supplied EventSetDescriptor
	 * 
	 * @param esd
	 *            the EventSetDescriptor
	 * @return true if the object will accept a connection
	 */
	public boolean connectionAllowed(EventSetDescriptor esd) {
		return connectionAllowed(esd.getName());
	}

	/**
	 * Notify this object that it has been registered as a listener with a
	 * source with respect to the supplied event name
	 * 
	 * @param eventName
	 *            the event
	 * @param source
	 *            the source with which this object has been registered as a
	 *            listener
	 */
	public synchronized void connectionNotification(String eventName,
			Object source) {
		if (connectionAllowed(eventName)) {
			if (eventName.compareTo("trainingSet") == 0) {
				m_trainingProvider = source;
			} else if (eventName.compareTo("testSet") == 0) {
				m_testProvider = source;
			} else if (eventName.compareTo("dataSet") == 0) {
				m_dataProvider = source;
			} else if (eventName.compareTo("instance") == 0) {
				m_instanceProvider = source;
			}
			m_connectedFormat = null;
		}
	}

	/**
	 * Notify this object that it has been deregistered as a listener with a
	 * source with respect to the supplied event name
	 * 
	 * @param eventName
	 *            the event
	 * @param source
	 *            the source with which this object has been registered as a
	 *            listener
	 */
	public synchronized void disconnectionNotification(String eventName,
			Object source) {

		if (eventName.compareTo("trainingSet") == 0) {
			if (m_trainingProvider == source) {
				m_trainingProvider = null;
			}
		}
		if (eventName.compareTo("testSet") == 0) {
			if (m_testProvider == source) {
				m_testProvider = null;
			}
		}
		if (eventName.compareTo("dataSet") == 0) {
			if (m_dataProvider == source) {
				m_dataProvider = null;
			}
		}

		if (eventName.compareTo("instance") == 0) {
			if (m_instanceProvider == source) {
				m_instanceProvider = null;
			}
		}
		m_connectedFormat = null;
	}

	public void setLog(weka.gui.Logger logger) {
		m_logger = logger;
	}

	public void stop() {
		// Pass on to upstream beans
		if (m_trainingProvider != null
				&& m_trainingProvider instanceof BeanCommon) {
			((BeanCommon) m_trainingProvider).stop();
		}

		if (m_testProvider != null && m_testProvider instanceof BeanCommon) {
			((BeanCommon) m_testProvider).stop();
		}

		if (m_dataProvider != null && m_dataProvider instanceof BeanCommon) {
			((BeanCommon) m_dataProvider).stop();
		}

		if (m_instanceProvider != null
				&& m_instanceProvider instanceof BeanCommon) {
			((BeanCommon) m_instanceProvider).stop();
		}
	}

	/**
	 * Returns true if. at this time, the bean is busy with some (i.e. perhaps a
	 * worker thread is performing some calculation).
	 * 
	 * @return true if the bean is busy.
	 */
	public boolean isBusy() {
		return false;
	}

	/**
	 * Returns true, if at the current time, the named event could be generated.
	 * Assumes that the supplied event name is an event that could be generated
	 * by this bean
	 * 
	 * @param eventName
	 *            the name of the event in question
	 * @return true if the named event could be generated at this point in time
	 */
	public boolean eventGeneratable(String eventName) {
		if (eventName.compareTo("trainingSet") == 0) {
			if (m_trainingProvider == null) {
				return false;
			} else {
				if (m_trainingProvider instanceof EventConstraints) {
					if (!((EventConstraints) m_trainingProvider)
							.eventGeneratable("trainingSet")) {
						return false;
					}
				}
			}
		}

		if (eventName.compareTo("dataSet") == 0) {
			if (m_dataProvider == null) {
				if (m_instanceProvider == null) {
					m_connectedFormat = null;
					notifyDataFormatListeners();
				}
				return false;
			} else {
				if (m_dataProvider instanceof EventConstraints) {
					if (!((EventConstraints) m_dataProvider)
							.eventGeneratable("dataSet")) {
						m_connectedFormat = null;
						notifyDataFormatListeners();
						return false;
					}
				}
			}
		}

		if (eventName.compareTo("instance") == 0) {
			if (m_instanceProvider == null) {
				if (m_dataProvider == null) {
					m_connectedFormat = null;
					notifyDataFormatListeners();
				}
				return false;
			} else {
				if (m_instanceProvider instanceof EventConstraints) {
					if (!((EventConstraints) m_instanceProvider)
							.eventGeneratable("instance")) {
						m_connectedFormat = null;
						notifyDataFormatListeners();
						return false;
					}
				}
			}
		}

		if (eventName.compareTo("testSet") == 0) {
			if (m_testProvider == null) {
				return false;
			} else {
				if (m_testProvider instanceof EventConstraints) {
					if (!((EventConstraints) m_testProvider)
							.eventGeneratable("testSet")) {
						return false;
					}
				}
			}
		}
		return true;
	}
}
